還記得 30 天前,我們從一個簡單的 expect(1 + 1)->toBe(2) 開始嗎?
今天,我們不僅完成了完整的 Todo API,更重要的是建立了 TDD 思維。這趟旅程的終點,正是你成為更好開發者的起點。
今天我們要完成最後一塊拼圖:
開始 → 部署前檢查 → CI/CD 設置 → 生產環境測試
  ↓        ↓           ↓            ↓
Day 1   Day 10      Day 20       Day 30
[基礎]  [進階]      [實戰]       [部署] ← 你在這裡!
進度:[████████████████████] 100% 🎉
在部署之前,讓我們確保所有測試都能順利通過:
# 執行所有測試
php artisan test
# 執行特定測試套件
php artisan test --testsuite=Feature
php artisan test --testsuite=Unit
# 生成測試覆蓋率報告
php artisan test --coverage --min=80
<?php
namespace Tests\Feature\Day30;
use Tests\TestCase;
use Illuminate\Support\Facades\Artisan;
it('passes all unit tests', function () {
    $result = Artisan::call('test', [
        '--testsuite' => 'Unit',
        '--stop-on-failure' => true
    ]);
    
    expect($result)->toBe(0);
});
it('passes all feature tests', function () {
    $result = Artisan::call('test', [
        '--testsuite' => 'Feature',
        '--stop-on-failure' => true
    ]);
    
    expect($result)->toBe(0);
});
it('has minimum test coverage', function () {
    $output = shell_exec('php artisan test --coverage --min=80 2>&1');
    
    expect($output)->not->toContain('Code coverage below');
});
it('has no pending migrations', function () {
    $result = Artisan::call('migrate:status');
    $output = Artisan::output();
    
    expect($output)->not->toContain('Pending');
});
it('can compile assets without errors', function () {
    $result = shell_exec('npm run build 2>&1');
    
    expect($result)->toContain('built in');
    expect($result)->not->toContain('ERROR');
});
name: Laravel CI/CD
on:
  push:
    branches: [ main, develop ]
  pull_request:
    branches: [ main ]
jobs:
  test:
    runs-on: ubuntu-latest
    services:
      mysql:
        image: mysql:8.0
        env:
          MYSQL_ROOT_PASSWORD: password
          MYSQL_DATABASE: laravel_test
    steps:
    - uses: actions/checkout@v3
    - name: Setup PHP
      uses: shivammathur/setup-php@v2
      with:
        php-version: '8.2'
        coverage: xdebug
    - name: Install Dependencies
      run: |
        composer install -q --no-ansi --no-interaction
        php artisan key:generate
    - name: Run Tests
      run: php artisan test --coverage --min=80
    - name: Deploy to Production
      if: github.ref == 'refs/heads/main'
      run: |
        # Deployment commands here
        php artisan config:cache
        php artisan route:cache
# 準備部署腳本
#!/bin/bash
cd /home/forge/todo-api
git pull origin main
composer install --no-dev --optimize-autoloader
php artisan migrate --force
php artisan config:cache
php artisan route:cache
php artisan view:cache
php artisan queue:restart
<?php
namespace Tests\Feature\Day30;
use Tests\TestCase;
it('validates critical endpoints', function () {
    $endpoints = [
        '/api/todos' => 200,
        '/api/health' => 200,
        '/api/invalid' => 404
    ];
    
    foreach ($endpoints as $endpoint => $expectedStatus) {
        $response = $this->get($endpoint);
        expect($response->status())->toBe($expectedStatus);
    }
});
it('monitors response time', function () {
    $start = microtime(true);
    $response = $this->get('/');
    $duration = (microtime(true) - $start) * 1000;
    
    expect($response->status())->toBe(200);
    expect($duration)->toBeLessThan(500);
});
<?php
namespace Tests\Feature\Day30;
use Tests\TestCase;
it('has correct environment configuration', function () {
    $requiredEnvVars = [
        'APP_NAME', 'APP_ENV', 'APP_KEY', 'APP_URL',
        'DB_CONNECTION', 'DB_HOST', 'DB_PORT', 'DB_DATABASE',
    ];
    
    foreach ($requiredEnvVars as $var) {
        expect(env($var))->not->toBeNull();
    }
});
it('uses secure settings in production', function () {
    if (app()->environment('production')) {
        expect(config('app.debug'))->toBeFalse();
        expect(config('app.url'))->toStartWith('https://');
        expect(config('session.secure'))->toBeTrue();
    }
});
// tests/Feature/Day30/PerformanceTest.php
<?php
namespace Tests\Feature\Day30;
use Tests\TestCase;
use Illuminate\Support\Facades\DB;
it('keeps database queries under threshold', function () {
    DB::enableQueryLog();
    $this->get('/api/todos');
    $queryCount = count(DB::getQueryLog());
    
    expect($queryCount)->toBeLessThanOrEqual(10);
});
it('maintains acceptable memory usage', function () {
    $startMemory = memory_get_usage();
    $this->post('/api/todos', ['title' => 'Test Todo']);
    $memoryUsed = (memory_get_usage() - $startMemory) / 1024 / 1024;
    
    expect($memoryUsed)->toBeLessThan(10);
});
<?php
namespace Tests\Feature\Day30;
use Tests\TestCase;
use Illuminate\Support\Facades\Log;
it('logs errors correctly', function () {
    Log::shouldReceive('error')
        ->once()
        ->with('Application error', \Mockery::any());
    
    // Trigger error logging
    Log::error('Application error', ['test' => true]);
});
it('returns appropriate error responses', function () {
    $response = $this->getJson('/api/invalid-endpoint');
    
    expect($response->status())->toBe(404);
});
讓我們回顧這 30 天的學習旅程:
# 執行測試並產生覆蓋率報告
php artisan test --coverage --min=80
php artisan test --coverage-html=coverage
恭喜你完成了這 30 天的 TDD 學習之旅!你現在已經具備了:
今天我們完成了整個 TDD 學習旅程的最後一哩路!從部署前的檢查、CI/CD 設置,到生產環境的監控,我們建立了一個完整的測試驅動開發流程。
30 天前,我們從一個簡單的測試開始;30 天後,我們擁有了一個經過完整測試、可以自信部署的應用程式。TDD 不僅改變了我們寫程式的方式,更重要的是改變了我們思考問題的角度。
感謝你陪伴我走完這 30 天的旅程。願 TDD 成為你開發路上的明燈!
Happy Testing, Happy Coding! 🚀
這是 iThome 鐵人賽「Laravel Pest TDD 實戰」系列的最後一篇文章。希望這 30 天的內容對你有所幫助。歡迎在實際專案中應用所學,並持續精進你的測試技能!